[SYSTEMDS-3863] Add PowerTransformer built-in functions - #2499
[SYSTEMDS-3863] Add PowerTransformer built-in functions#2499WenliangCao wants to merge 14 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2499 +/- ##
============================================
+ Coverage 71.61% 71.65% +0.04%
- Complexity 50352 50490 +138
============================================
Files 1626 1628 +2
Lines 194664 195069 +405
Branches 38007 38042 +35
============================================
+ Hits 139408 139782 +374
- Misses 44328 44364 +36
+ Partials 10928 10923 -5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Add two DML builtins for the midterm PowerTransformer prototype: - powerTransform.dml estimates one Yeo-Johnson lambda per input column, uses lambda = 1 for constant columns, and applies the transform. - powerTransformApply.dml applies the Yeo-Johnson transform with supplied per-column lambdas. Register both builtins so they can be resolved by SystemDS. Add powerTransformSmokeTest.dml to verify: - powerTransformApply with lambda = 1 behaves as identity - powerTransform returns output dimensions matching the input - one lambda is returned per input column - constant columns use lambda = 1 - transformed output and lambdas do not contain NaN or Inf The smoke test passes with: ./bin/systemds src/test/scripts/functions/builtin/powerTransformSmokeTest.dml
Add a focused PowerTransformApply test that compares the DML builtin against
an independent R reference implementation.
The test adds:
- powerTransformApply.dml as a small DML wrapper around the registered builtin
- powerTransformApply.R as the reference Yeo-Johnson apply implementation
- BuiltinPowerTransformTest.java to run the DML and R scripts and compare Y
The test covers the key apply branches with fixed lambdas:
- lambda = 0 for the positive log branch
- lambda = 1 for the identity-style middle case
- lambda = 2 for the negative log branch
Also translate the remaining PowerTransformer comments from Chinese to English
in the prototype DML files.
Verified with:
Rscript -e 'parse(file="src/test/scripts/functions/builtin/powerTransformApply.R"); cat("R syntax OK\n")'
./bin/systemds src/test/scripts/functions/builtin/powerTransformSmokeTest.dml
mvn -Dtest=BuiltinPowerTransformTest test
130c741 to
753c5a0
Compare
gaturchenko
left a comment
There was a problem hiding this comment.
Hey @WenliangCao, thank you very much for the contribution and extensive experiments. I have left some comments, please address them so that we can merge your code into the main. The main issue however is a failing test, we cannot merge if the CI is red, so please address it first. Feel free to reach out via an email or right here in the PR thread if anything is unclear or if you disagree with some of my comments, we can absolutely discuss anything.
|
|
||
| # Apply one transform with this lambda and use the temporary y for scoring | ||
| emptyStats = matrix(0.0, rows=0, cols=0) | ||
| y = powerTransformApply(x, lambdaMatrix, emptyStats, emptyStats, method); |
There was a problem hiding this comment.
This has a fixed invocation overhead, and for matrices with, say, 1000 rows and 4 columns, this will dominate the runtime, so it should be inlined somehow
|
|
||
| # Jacobian term for the selected transformation | ||
| if (method == "box-cox") { | ||
| jacobian = (lambda - 1.0) * sum(log(x)); |
There was a problem hiding this comment.
Only lambda changes here, x is always the same, so you can compute sum(log(x)) just once, which can be done with a separate function. Then you can add something like jacTerm as another input to ptNegLogLikelihood and get jacobian = (lambda - 1.0) * jacTerm;. This should improve performance substantially for large matrices
| jacobian = (lambda - 1.0) * sum(log(x)); | ||
| } | ||
| else { | ||
| jacobian = (lambda - 1.0) * sum(sign(x) * log(abs(x) + 1.0)); |
There was a problem hiding this comment.
Same logic as for the comment above
|
|
||
| private void runPowerTransformTest( | ||
| String method, boolean standardize, double[][] input, boolean shouldFail) { | ||
| ExecMode oldExecMode = setExecMode(ExecType.CP); |
There was a problem hiding this comment.
You only add CP coverage, would be good to have Spark covered as well. Let us know if there are particular issues with Spark for Power Transform however
| } | ||
|
|
||
| @Test | ||
| public void testPowerTransformYeoJohnsonLambdaAboveInitialInterval() { |
There was a problem hiding this comment.
This test fails both in CI and for me locally
There was a problem hiding this comment.
Implementing powerTransform like this defeats the purpose of R tests, as we want to compare against an actual existing R implementation. A better way of doing this is to actually find an R package that implements powerTransform with your 2 methods and compare against it
Add the required blank line after the Apache license headers in the PowerTransformer fit and apply built-ins. Remove the redundant -exec singlenode arguments from the fit and apply tests, which already select the execution mode through the test framework. Apply dev/format-changed.sh against upstream/main to format PR-edited Java lines with dev/CodeStyle_eclipse.xml and address the Java Format Check failure.
|
In addition the format check fails @WenliangCao, please run |
a9cde52 to
5df732f
Compare
5df732f to
280e878
Compare
Summary
This pull request adds PowerTransformer built-ins with separate fit and apply workflows. The implementation supports Yeo-Johnson and Box-Cox transformations, per-column parameter estimation, optional standardization, and reuse of fitted parameters on new data.
Yeo-Johnson is the default method and supports mixed-sign inputs. Box-Cox is available for strictly positive inputs.
API
powerTransformestimates one lambda per column and optionally standardizes the transformed columns.powerTransformApplyreuses the fitted lambdas, means, and scales without re-estimation.Implementation
powerTransform.dmlfor fitting and transforming feature matrices.powerTransformApply.dmlfor applying fitted transformations.[-2, 2]as an initial bracket and expand it when necessary, allowing the optimum to lie outside the initial interval.Validation
mvn -Dtest=BuiltinPowerTransformTest testThe tests cover:
[-2, 2]interval;Numerical outputs are compared with independent R reference implementations.
Documentation
User-facing signatures, arguments, return values, input requirements, and examples are documented in
docs/site/builtins-reference.md.Experiments
The reproducible evaluation compares no scaling, standard scaling, robust scaling, Yeo-Johnson, and Box-Cox on nine public datasets. Each experiment uses the fixed seeds
13,37,73,101, and149.n_init=20)Supervised tasks use 80/20 train/test splits; classification is stratified and Parkinsons Telemonitoring uses subject-level group splits. Each transformation is fitted on training data and applied to test data. Clustering uses the full dataset.
The table reports the mean primary metric over five runs. A dash indicates that Box-Cox is not applicable because the feature matrix contains non-positive values.
Power transformations rank first or tie for first on four of the nine datasets. Representative results include:
0.6486with standard scaling to0.6114.0.9618.0.7759.0.5330, compared with0.1873for standard scaling.Reproducible artifacts
Limitations
The downstream effect of preprocessing is dataset- and estimator-dependent. The evaluation uses fixed distance-based estimators, three datasets per task, and no hyperparameter search or statistical significance test. The results therefore demonstrate practical cases where power transformations help, rather than universal superiority over other preprocessing methods.
Related issue
SYSTEMDS-3863